package org.fujene.memcon;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import org.fujene.diskblock.DiskBlock;
import org.fujene.diskblock.SkipPointerDiskBlock;
import org.fujene.diskblock.local.ContentDiskBlock;
import org.fujene.diskblock.local.DictionaryDiskBlock;
import org.fujene.diskblock.local.InvertIndexDiskBlock;
import org.fujene.memoryblock.MemoryBlock;
import org.fujene.memoryblock.SkipPointerBlock;
import org.fujene.memoryblock.local.LocalContentBlock;
import org.fujene.memoryblock.local.LocalDictionaryBlock;
import org.fujene.memoryblock.local.LocalInvertIndexBlock;
import org.fujene.structure.Constants;
import org.fujene.structure.LookupPointer;
import org.fujene.structure.SearchedTerm;

/**
 * インデックス保存時のメモリ情報をにないます。 ここでのデータ処理はできるだけ避け、最小限のデータ読み書き のみ対応させます。
 */
public class LocalIndexMemory{
	private static final int MEM_TYPES = Constants.MEMORY_TYPE_KINDS; // そのうち拡張する予定
	private static final int MEM_DICT = Constants.MEMORY_TYPE_DICT;
	private static final int MEM_INVINDEX = Constants.MEMORY_TYPE_INVINDEX;
	private static final int MEM_SKIPPTR = Constants.MEMORY_TYPE_SKIPPTR;
	private static final int MEM_CONT = Constants.MEMORY_TYPE_CONTENTS;
	private static final int DEFAULT_OPT_LEN = 1;

	// クラスメンバ ブロックをなんとか表す

	// ブロック本体
	// 各ブロックのサイズ
	private int blockSize = 1048576; // 1MB
	// ブロック数
	private int blockNum = 256;

	private MemoryBlock[] blocks = null;
	private int[] writeBackIDs = null;
	private int nextID = 0;

	private int[] firstBlock = null;
	private int[] lastBlock = null;

	// ディスクとの連絡用
	private IndexDisk disk = null;
	private String dir = null;

	// TODO: 次のポインタここだから
	private LookupPointer[] futurePointers = null;

	public LocalIndexMemory(){
		this(1048576, 256);
	}

	public LocalIndexMemory(int blockSize, int blockNum){
		this(blockSize, blockNum, null);
	}

	public LocalIndexMemory(int blockSize, int blockNum, IndexDisk disk){
		setBlockSize(blockSize);
		setBlockNum(blockNum);
		this.disk = disk;
		this.dir = disk.getDir();
		initBlock();
	}

	// 一番最初に使おう
	private void initBlock(){
		this.blocks = new MemoryBlock[this.blockNum];
		this.writeBackIDs = new int[MEM_TYPES];

		this.firstBlock = new int[MEM_TYPES];
		this.lastBlock = new int[MEM_TYPES];

		this.futurePointers = new LookupPointer[MEM_TYPES];

		for(int i = 0; i < MEM_TYPES; i++){
			this.firstBlock[i] = -1;
			this.lastBlock[i] = -1;
			this.writeBackIDs[i] = -1;
			this.futurePointers[i] = Constants.EMPTY_PTR;
		}

		initializeAllocate();
	}

	// 初期アロケート 種類追加時はここをいじる
	private void initializeAllocate(){
		allocateBlock(MEM_DICT);
		allocateBlock(MEM_SKIPPTR);
		allocateBlock(MEM_INVINDEX);
		allocateBlock(MEM_CONT);
	}

	private int allocateBlock(int type){
		for(int i = 0; i < blocks.length; i++){
			if (i == blocks.length - 1 && blocks[i] != null){
				int mover = -1;
				// メモリの空きがなかったのでディスクに寄せる
				// 優先順: Contents -> Inv. Index -> (TODO: →はしばらく凍結)Dictionary ->
				// Skip ptr.
				if (getMemoryAmount(MEM_CONT) > 2){
					mover = getMinimumBlock(MEM_CONT);
					saveMemory(mover);
					i = mover;
				}else if (getMemoryAmount(MEM_INVINDEX) > 2){
					mover = getMinimumBlock(MEM_INVINDEX);
					saveMemory(mover);
					i = mover;
					// } else if (getMemoryAmount(MEM_DICT) > 2) {
					// mover = getMinimumBlock(MEM_DICT);
					// saveMemory(mover);
					// i = mover;
					// } else if (getMemoryAmount(MEM_SKIPPTR) > 2) {
					// mover = getMinimumBlock(MEM_SKIPPTR);
					// saveMemory(mover);
					// i = mover;
				}
			}

			if (blocks[i] == null){
				switch(type){
				case MEM_DICT:
					blocks[i] = new LocalDictionaryBlock(blockSize, nextID++, true);
					if (firstBlock[MEM_DICT] == -1) firstBlock[MEM_DICT] = i;
					lastBlock[MEM_DICT] = i;
					break;
				case MEM_SKIPPTR:
					blocks[i] = new SkipPointerBlock(blockSize, nextID++, true);
					if (firstBlock[MEM_SKIPPTR] == -1) firstBlock[MEM_SKIPPTR] = i;

					if (lastBlock[MEM_SKIPPTR] != -1) blocks[lastBlock[MEM_SKIPPTR]].setOption(1, nextID);
					blocks[i].setOption(1, -1);
					blocks[i].setOption(2, MEM_SKIPPTR);
					lastBlock[MEM_SKIPPTR] = i;
					break;
				case MEM_INVINDEX:
					blocks[i] = new LocalInvertIndexBlock(blockSize, nextID++, true);
					if (firstBlock[MEM_INVINDEX] == -1) firstBlock[MEM_INVINDEX] = i;
					lastBlock[MEM_INVINDEX] = i;
					break;
				case MEM_CONT:
					blocks[i] = new LocalContentBlock(blockSize, nextID++, true);
					if (firstBlock[MEM_CONT] == -1) firstBlock[MEM_CONT] = i;
					lastBlock[MEM_CONT] = i;
					break;
				default:
					// タイプ外
					throw new IndexOutOfBoundsException();
				}

				return i;
			}
		}

		return -1;
	}

	private int getMemoryAmount(int type){
		int amount = 0;

		for(int i = 0; i < blocks.length; i++){
			if (blocks[i] != null && blocks[i].getType() == type) amount++;
		}

		return amount;
	}

	private int getBlockArrayIndex(int ID){
		for(int i = 0; i < blocks.length; i++){
			if (blocks[i] == null) continue;
			if (blocks[i].getID() == ID) return i;
		}
		// なかった
		return -1;
	}

	// 辞書読み込み
	public SearchedTerm lookupDictionary(LookupPointer ptr, int valueType){
		if (ptr == null || ptr.block == -1 || ptr.point == -1) return null;
		int arrayIndex = getBlockArrayIndex(ptr.block);
		if (arrayIndex != -1){
			return ((LocalDictionaryBlock)blocks[arrayIndex]).getTerm(ptr.point);
		}else{
			// ディスク探索
			if (disk.isExist(ptr.block)){
				try{
					DictionaryDiskBlock dicDisk = new DictionaryDiskBlock(disk.getFileMap().get(
							Integer.valueOf(ptr.block)));
					return dicDisk.getTerm(ptr.point);
				}
				catch(Exception e){}
			}
		}

		return null;
	}

	// 辞書ポインタだけ読み込み
	public LookupPointer lookupDictionaryNext(LookupPointer ptr){
		int arrayIndex = getBlockArrayIndex(ptr.block);

		if (arrayIndex != -1){
			return ((LocalDictionaryBlock)blocks[arrayIndex]).getNextPointer(ptr.point);
		}else{
			if (disk.isExist(ptr.block)){
				try{
					DictionaryDiskBlock dicDisk = new DictionaryDiskBlock(disk.getFileMap().get(
							Integer.valueOf(ptr.block)));
					return dicDisk.getNextPointer(ptr.point);
				}
				catch(Exception e){}
			}
		}

		return null;
	}

	// 転置インデックスポインタだけ読み込み
	public LookupPointer lookupDictionaryInvIndex(LookupPointer ptr){
		int arrayIndex = getBlockArrayIndex(ptr.block);

		if (arrayIndex != -1){
			return ((LocalDictionaryBlock)blocks[arrayIndex]).getDocIDPointer(ptr.point);
		}else{
			if (disk.isExist(ptr.block)){
				try{
					DictionaryDiskBlock dicDisk = new DictionaryDiskBlock(disk.getFileMap().get(
							Integer.valueOf(ptr.block)));
					return dicDisk.getDocIDPointer(ptr.point);
				}
				catch(Exception e){}
			}
		}

		return null;
	}

	public LookupPointer storeDictionary(String str, LookupPointer next) throws UnsupportedEncodingException{
		int dictLast = lastBlock[MEM_DICT];

		LocalDictionaryBlock dicBlock = (LocalDictionaryBlock)blocks[dictLast];
		// 置き場探し
		int newPtr = dicBlock.getNewPointer(str);
		if (newPtr == -1){
			// 他をあたる
			int nextBlock = allocateBlock(MEM_DICT);
			if (nextBlock != -1){
				dictLast = nextBlock;
				dicBlock = (LocalDictionaryBlock)blocks[nextBlock];
				newPtr = 0;
			}
		}

		dicBlock.setTerm(newPtr, next, Constants.EMPTY_PTR, str);
		writeBackIDs[MEM_DICT]++;
		return new LookupPointer(dicBlock.getID(), newPtr);
	}

	// ※追加しない
	public void storeDictionaryPointer(LookupPointer dicPtr, LookupPointer next){
		int arrayIndex = getBlockArrayIndex(dicPtr.block);

		if (arrayIndex != -1){
			try{
				((LocalDictionaryBlock)blocks[arrayIndex]).setNextPointer(dicPtr.point, next);
			}
			catch(Exception e){}
		}else{}
	}

	// ※追加しない
	public void storeDictionaryDocIDPtr(LookupPointer dicPtr, LookupPointer docID){
		int arrayIndex = getBlockArrayIndex(dicPtr.block);

		if (arrayIndex != -1){
			try{
				((LocalDictionaryBlock)blocks[arrayIndex]).setDocIDptr(dicPtr.point, docID);
			}
			catch(Exception e){}
		}else{}
	}

	// 本文スキップポインタ対応
	public LookupPointer lookupSkipPointer(int index, int type){
		int nextBlockNum = firstBlock[type];
		SkipPointerBlock skipBlock = null;
		int blockIndex = getBlockArrayIndex(nextBlockNum);
		int findLength = 0;
		int length = 0;

		// ブロック探索
		skipBlock = (SkipPointerBlock)blocks[blockIndex];
		// スキップポインタなし
		if (skipBlock.getLength() == 0) return null;

		// インデックス探し
		do{
			length = skipBlock.getLength();
			findLength += length;
			if (findLength >= index) break;
			nextBlockNum = skipBlock.getNextblock();
			blockIndex = getBlockArrayIndex(nextBlockNum);
			skipBlock = (SkipPointerBlock)blocks[blockIndex];
		}while(nextBlockNum != -1);

		// そんなになかった
		if (findLength < index || skipBlock == null) return null;

		index -= (findLength - length);
		return skipBlock.getPointer(index);
	}

	// 本文スキップポインタ対応
	public int getSkipPointerLength(int type){
		int nextBlockNum = firstBlock[type];
		SkipPointerBlock skipBlock = null;
		int blockIndex = -1;
		int findIndex = 0;

		// ブロック探索
		do{
			blockIndex = getBlockArrayIndex(nextBlockNum);
			skipBlock = (SkipPointerBlock)blocks[blockIndex];
			int length = skipBlock.getLength();
			findIndex += length;
			nextBlockNum = skipBlock.getNextblock();
		}while(nextBlockNum != -1);

		return findIndex;
	}

	// 本文スキップポインタ対応
	public void storeSkipPointer(int index, int type, LookupPointer ptr, boolean isModify){
		int nextBlockNum = firstBlock[type];
		int blockIndex = -1;
		int findIndex = 0;
		SkipPointerBlock skipBlock = null;
		SkipPointerBlock setBlock = null;

		// 置き場探し
		do{
			blockIndex = getBlockArrayIndex(nextBlockNum);
			skipBlock = (SkipPointerBlock)blocks[blockIndex];
			int length = skipBlock.getLength();
			findIndex += length;
			if (findIndex - index >= 0){
				setBlock = skipBlock;
				break;
			}
			nextBlockNum = skipBlock.getNextblock();
		}while(nextBlockNum != -1);

		if (setBlock != null){
			if (isModify)
				setBlock.setPointer(index, ptr, isModify);
			else{
				byte[] moveVal = null;
				moveVal = setBlock.setPointer(index, ptr, isModify);
				if (moveVal != null){
					do{
						nextBlockNum = skipBlock.getNextblock();
						blockIndex = getBlockArrayIndex(nextBlockNum);
						setBlock = (SkipPointerBlock)blocks[blockIndex];
						moveVal = setBlock.movePointerRep(moveVal);
					}while(moveVal != null);
				}
			}
		}

		writeBackIDs[type]++;
	}

	// デバッグ用 スキップポインタ一覧を出力
	// 本文スキップポインタ対応
	public void showSkipPointer(int type){
		int nextBlockNum = firstBlock[type];
		SkipPointerBlock skipBlock = null;
		int blockIndex = -1;
		LookupPointer output = null;

		// ブロック探索
		do{
			blockIndex = getBlockArrayIndex(nextBlockNum);
			skipBlock = (SkipPointerBlock)blocks[blockIndex];
			int length = skipBlock.getLength();
			for(int i = 0; i < length; i++){
				output = skipBlock.getPointer(i);
				int dicBlock = getBlockArrayIndex(output.block);
				SearchedTerm newTerm = ((LocalDictionaryBlock)blocks[dicBlock]).getTerm(output.point);
				newTerm.setSkipPtrIndex(i);
				System.out.println("Term at skip pointer: " + newTerm.toString());
			}
			nextBlockNum = skipBlock.getNextblock();
		}while(nextBlockNum != -1);
	}

	public LookupPointer[] lookupInvertIndex(LookupPointer ptr){
		int arrayIndex = getBlockArrayIndex(ptr.block);

		if (arrayIndex != -1){
			return ((LocalInvertIndexBlock)blocks[arrayIndex]).getInvertIndex(ptr.point);
		}else{
			if (disk.isExist(ptr.block)){
				try{
					File desc = disk.getDescriptor(ptr.block);
					InvertIndexDiskBlock invDisk = new InvertIndexDiskBlock(ptr.block, desc, blockSize,
							DEFAULT_OPT_LEN);
					return invDisk.getInvertIndex(ptr.point);
				}
				catch(Exception e){}
			}
		}
		System.out.println("Failed. " + ptr.toString());

		return null;
	}

	public LookupPointer storeInvertIndex(LookupPointer prev, LookupPointer content){
		int invertLast = lastBlock[MEM_INVINDEX];

		LocalInvertIndexBlock invBlock = (LocalInvertIndexBlock)blocks[invertLast];
		// 置き場探し
		int newPtr = invBlock.getLeft();
		if (newPtr == 0){
			// 他をあたる
			int nextBlock = allocateBlock(MEM_INVINDEX);
			if (nextBlock != -1){
				invertLast = nextBlock;
				invBlock = (LocalInvertIndexBlock)blocks[nextBlock];
			}
		}
		writeBackIDs[MEM_INVINDEX]++;

		return invBlock.setInvertIndex(prev, content);
	}

	public String lookupContents(LookupPointer ptr){
		int arrayIndex = getBlockArrayIndex(ptr.block);

		if (arrayIndex != -1){
			return ((LocalContentBlock)blocks[arrayIndex]).getContent(ptr.point);
		}else{
			if (disk.isExist(ptr.block)){
				try{
					File fileDesc = disk.getDescriptor(ptr.block);
					ContentDiskBlock contDisk = new ContentDiskBlock(ptr.block, fileDesc, blockSize,
							DEFAULT_OPT_LEN);
					return contDisk.getContent(ptr.point);
				}
				catch(Exception e){}
			}
		}

		return null;
	}

	public LookupPointer storeContents(String content){
		int contentLast = lastBlock[MEM_CONT];

		LocalContentBlock contBlock = (LocalContentBlock)blocks[contentLast];
		// 置き場探し
		int newPtr = contBlock.getNewPointer(content);
		if (newPtr == -1){
			// 他をあたる
			int nextBlock = allocateBlock(MEM_CONT);
			if (nextBlock != -1){
				contentLast = nextBlock;
				contBlock = (LocalContentBlock)blocks[nextBlock];
				newPtr = 0;
			}
		}
		contBlock.setContent(newPtr, content);
		writeBackIDs[MEM_CONT]++;
		return new LookupPointer(contBlock.getID(), newPtr);
	}

	public void readMemory() throws Exception{
		// 記録を読み出し
		// コピーコンストラクタ
		SortedMap<Integer, File> fileMap = new TreeMap<Integer, File>(disk.getFileMap());
		Set<Integer> fileNums = fileMap.keySet();

		System.out.print("Reading memory... ");
		for(Integer file: fileNums){
			int ID = file.intValue();
			int type = disk.getFileType(ID);
			int newID;
			switch(type){
			case MEM_DICT:
				DictionaryDiskBlock dicBlock = new DictionaryDiskBlock(fileMap.get(file));
				newID = getBlockArrayIndex(ID);
				if (newID == -1) newID = allocateBlock(MEM_DICT);
				dicBlock.readToMemory((LocalDictionaryBlock)blocks[newID]);
				break;

			case MEM_SKIPPTR:
				SkipPointerDiskBlock skipBlock = new SkipPointerDiskBlock(fileMap.get(file));
				newID = getBlockArrayIndex(ID);
				if (newID == -1) newID = allocateBlock(MEM_SKIPPTR);
				skipBlock.readToMemory((SkipPointerBlock)blocks[newID]);
				break;

			case MEM_INVINDEX:
				InvertIndexDiskBlock invBlock = new InvertIndexDiskBlock(fileMap.get(file));
				newID = getBlockArrayIndex(ID);
				if (newID == -1) newID = allocateBlock(MEM_INVINDEX);
				invBlock.readToMemory((LocalInvertIndexBlock)blocks[newID]);
				break;

			case MEM_CONT:
				ContentDiskBlock contBlock = new ContentDiskBlock(fileMap.get(file));
				newID = getBlockArrayIndex(ID);
				if (newID == -1) newID = allocateBlock(MEM_CONT);
				contBlock.readToMemory((LocalContentBlock)blocks[newID]);
				break;

			default:
				break;
			}
		}

		// 読み込んだブロックは消去
		for(int i = 0; i < blocks.length; i++){
			if (blocks[i] != null){
				disk.dropFile(blocks[i].getID());
			}
		}

		disk.reloadFileList();

		System.out.println("Done.");
	}

	public void saveMemory(int index){
		if (blocks[index] != null){
			// フラグを立てておく
			blocks[index].setAboutToFlush(true);
			// 既存のファイルがあれば消去
			disk.dropFile(blocks[index].getID());
			switch(blocks[index].getType()){
			case MEM_DICT:
				DictionaryDiskBlock dicBlock = new DictionaryDiskBlock((LocalDictionaryBlock)blocks[index],
						dir);
				dicBlock.save();
				break;

			case MEM_SKIPPTR:
				SkipPointerDiskBlock skipBlock = new SkipPointerDiskBlock((SkipPointerBlock)blocks[index],
						dir);
				skipBlock.save();
				break;

			case MEM_INVINDEX:
				InvertIndexDiskBlock invBlock = new InvertIndexDiskBlock(
						(LocalInvertIndexBlock)blocks[index], dir);
				invBlock.save();
				break;

			case MEM_CONT:
				ContentDiskBlock contBlock = new ContentDiskBlock((LocalContentBlock)blocks[index], dir);
				contBlock.save();
				break;

			default:
				break;
			}
			// ブロック消去
			blocks[index] = null;
			disk.reloadFileList();
		}
	}

	public void saveMemories(){
		// 今までの記録を保存
		System.out.print("Writing to memory... ");
		for(int i = 0; i < blocks.length; i++){
			saveMemory(i);
		}

		System.out.println("Done.");
	}

	public int getMinimumBlock(int type){
		int minimum = Integer.MAX_VALUE;
		int minimumArray = -1;
		for(int i = 0; i < blocks.length; i++){
			if (blocks[i] != null && blocks[i].getType() == type && blocks[i].getID() < minimum){
				minimum = blocks[i].getID();
				minimumArray = i;
			}
		}

		return minimumArray;
	}

	// デバッグ用
	public void showMemoryElapsed(){
		System.out.println("--  Disk  --");
		Set<Integer> keys = disk.getFileMap().keySet();
		for(Integer diskID: keys){
			DiskBlock diskBlock;
			try{
				File desc = disk.getFileMap().get(diskID);
				diskBlock = new DiskBlock(desc){
					@Override
					public void readToMemory(MemoryBlock memBlock){
						throw new NoSuchMethodError("Unimplemented method");
					}

					@Override
					public byte[] getData(){
						throw new NoSuchMethodError("Unimplemented method");
					}
				};
				System.out.println(diskID + ": " + desc.length() + " " + diskBlock.getTypeStr());
			}
			catch(Exception e){}
		}

		System.out.println("-- Memory --");
		for(int i = 0; i < blocks.length; i++){
			if (blocks[i] != null){
				System.out.println(blocks[i].getID() + ": " + blocks[i].getUsed() + " "
						+ blocks[i].getClassName());
			}
		}
	}

	// デバッグ用
	public void showMemoryType(){
		for(int i = 0; i < blocks.length; i++){
			if (blocks[i] != null){
				System.out.println(i + ": " + blocks[i].getClassName());
			}
		}
	}

	public void setBlockSize(int blockSize){
		this.blockSize = blockSize;
	}

	public void setBlockNum(int blockNum){
		this.blockNum = blockNum;
	}

	public int getBlockSize(){
		return blockSize;
	}

	public int getBlockNum(){
		return blockNum;
	}

	public int getContentCount() throws Exception{
		int result = 0;
		// メモリ
		for(int i = 0; i < blocks.length; i++){
			if (blocks[i] != null && blocks[i] instanceof LocalContentBlock){
				result += ((LocalContentBlock)blocks[i]).getCount();
			}
		}

		// ディスク
		Collection<File> files = disk.getFileMap().values();
		for(File file: files){
			ContentDiskBlock blk = new ContentDiskBlock(file);

			if (blk.getType() == MEM_CONT){
				result += blk.getCount();
			}
		}

		return result;
	}

	public int getTermCount() throws Exception{
		int result = 0;
		for(int i = 0; i < blocks.length; i++){
			if (blocks[i] != null && blocks[i] instanceof LocalDictionaryBlock){
				int count = ((LocalDictionaryBlock)blocks[i]).getTermCount();
				result += count;
			}
		}

		// ディスク
		Collection<File> files = disk.getFileMap().values();
		for(File file: files){
			DictionaryDiskBlock blk = new DictionaryDiskBlock(file);

			if (blk.getType() == MEM_DICT){
				result += blk.getTermCount();
			}
		}

		return result;
	}
}